package com.zlx.mall.basic.web.model;


import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.util.*;
import org.springframework.web.util.WebUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author qingke
 * @see org.springframework.web.util.ContentCachingRequestWrapper
 * @see org.springframework.http.server.ServletServerHttpRequest
 * @see org.springframework.web.context.request.ServletWebRequest
 * @see org.springframework.web.util.UriComponentsBuilder
 * @see org.springframework.web.filter.ForwardedHeaderFilter
 **/
public class RequestWrapper extends HttpServletRequestWrapper {

    /** Try to parse remote address */
    private static final String[] HEADERS_TO_TRY = {
            "X-Forwarded-For", "X-Real-IP",
            "Proxy-Client-IP", "WL-Proxy-Client-IP",
            "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED",
            "HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP",
            "HTTP_FORWARDED_FOR", "HTTP_FORWARDED",
            "HTTP_VIA", "REMOTE_ADDR"};

    /** Unknown remote address */
    private static final String UNKNOWN_IP = "unknown";

    private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");

    private static final String FORWARDED_VALUE = "\"?([^;,\"]+)\"?";

    private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:for)=" + FORWARDED_VALUE);

    private URI uri;

    private MultiValueMap<String, String> uriVars;

    private HttpHeaders headers;

    private String remoteAddress;

    private HttpMethod method;

    private String contentType;

    private final FastByteArrayOutputStream content;

    private final StopWatch stopWatch = new StopWatch();

    /**
     * Constructs a httpServletRequest object wrapping the given httpServletRequest.
     *
     * @param httpServletRequest The httpServletRequest to wrap
     */
    public RequestWrapper(HttpServletRequest httpServletRequest) throws IOException {
        super(httpServletRequest);
        int contentLength = httpServletRequest.getContentLength();
        this.content = new FastByteArrayOutputStream(contentLength > 0 ? contentLength : 1024);
    }

    public void startStopWatch() {
        this.stopWatch.start();
        this.stopWatch.setKeepTaskList(false);
    }

    public long getStopWatchTime() {
        this.stopWatch.stop();
        return this.stopWatch.getTotalTimeMillis();
    }

    public URI getUri() {
        if (this.uri == null) {
            String urlString = null;
            boolean hasQuery = false;
            try {
                StringBuffer url = super.getRequestURL();
                String query = super.getQueryString();
                hasQuery = StringUtils.hasText(query);
                if (hasQuery) {
                    url.append('?').append(query);
                }
                urlString = url.toString();
                this.uri = new URI(urlString);
            } catch (URISyntaxException ex) {
                if (!hasQuery) {
                    throw new IllegalStateException(
                            "Could not resolve HttpServletRequest as URI: " + urlString, ex);
                }
                // Maybe a malformed query string... try plain request URL
                try {
                    urlString = super.getRequestURL().toString();
                    this.uri = new URI(urlString);
                } catch (URISyntaxException ex2) {
                    throw new IllegalStateException(
                            "Could not resolve HttpServletRequest as URI: " + urlString, ex2);
                }
            }
        }
        return this.uri;
    }

    public MultiValueMap<String, String> getUriVars() {
        if (this.uriVars == null) {
            this.uriVars = new LinkedMultiValueMap<>(16);
            URI uri = getUri();
            if (uri != null) {
                String query = uri.getQuery();
                if (query != null) {
                    Matcher matcher = QUERY_PARAM_PATTERN.matcher(query);
                    while (matcher.find()) {
                        String name = matcher.group(1);
                        String eq = matcher.group(2);
                        String value = matcher.group(3);
                        this.uriVars.add(name, (value != null ? value : (StringUtils.hasLength(eq) ? "" : null)));
                    }
                }
            }
        }
        return this.uriVars;
    }

    public HttpHeaders getHeaders() {
        if (this.headers == null) {
            this.headers = new HttpHeaders();

            for (Enumeration<String> headerNames = super.getHeaderNames(); headerNames.hasMoreElements(); ) {
                String headerName = headerNames.nextElement();
                headers.put(headerName, Collections.list(super.getHeaders(headerName)));
            }

            // HttpServletRequest exposes some headers as properties:
            // we should include those if not already present
            try {
                MediaType contentType = this.headers.getContentType();
                if (contentType == null) {
                    String requestContentType = getContentType();
                    if (StringUtils.hasLength(requestContentType)) {
                        contentType = MediaType.parseMediaType(requestContentType);
                        this.headers.setContentType(contentType);
                    }
                }
                if (contentType != null && contentType.getCharset() == null) {
                    String requestEncoding = super.getCharacterEncoding();
                    if (StringUtils.hasLength(requestEncoding)) {
                        Charset charset = Charset.forName(requestEncoding);
                        Map<String, String> params = new LinkedCaseInsensitiveMap<>();
                        params.putAll(contentType.getParameters());
                        params.put("charset", charset.toString());
                        MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
                        this.headers.setContentType(mediaType);
                    }
                }
            } catch (InvalidMediaTypeException ex) {
                // Ignore: simply not exposing an invalid content type in HttpHeaders...
            }

            if (this.headers.getContentLength() < 0) {
                int requestContentLength = super.getContentLength();
                if (requestContentLength != -1) {
                    this.headers.setContentLength(requestContentLength);
                }
            }
        }

        return this.headers;
    }

    protected String parseForwardedForRemoteAddress(HttpHeaders headers) {
        String forwardedHeader = headers.getFirst("Forwarded");
        if (StringUtils.hasText(forwardedHeader)) {
            String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0];
            Matcher matcher = FORWARDED_FOR_PATTERN.matcher(forwardedToUse);
            if (matcher.find()) {
                String value = matcher.group(1).trim();
                String host = value;
                int portSeparatorIdx = value.lastIndexOf(':');
                int squareBracketIdx = value.lastIndexOf(']');
                if (portSeparatorIdx > squareBracketIdx) {
                    if (squareBracketIdx == -1 && value.indexOf(':') != portSeparatorIdx) {
                        throw new IllegalArgumentException("Invalid IPv4 address: " + value);
                    }
                    host = value.substring(0, portSeparatorIdx);
                }
                return host;
            }
        }

        String forHeader = headers.getFirst("X-Forwarded-For");
        if (StringUtils.hasText(forHeader)) {
            return StringUtils.tokenizeToStringArray(forHeader, ",")[0];
        }

        return null;
    }

    protected String tryParseRemoteAddress(HttpHeaders headers, String defaultValue) {
        String remoteAddress = parseForwardedForRemoteAddress(headers);
        if (remoteAddress != null) {
            return remoteAddress;
        }
        for (String header : HEADERS_TO_TRY) {
            String ip = headers.getFirst(header);
            if (ip != null && ip.length() != 0 && !UNKNOWN_IP.equalsIgnoreCase(ip)) {
                String[] ips = StringUtils.tokenizeToStringArray(ip, ",");
                if (ips.length > 0) {
                    return ips[0];
                }
            }
        }

        return defaultValue;
    }

    public String getRemoteAddress() {
        if (this.remoteAddress == null) {
            this.remoteAddress = tryParseRemoteAddress(getHeaders(), super.getRemoteAddr());
        }
        return this.remoteAddress;
    }

    public HttpMethod getHttpMethod() {
        if (this.method == null) {
            this.method = HttpMethod.resolve(super.getMethod());
        }
        return this.method;
    }

    @Override
    public String getCharacterEncoding() {
        String enc = super.getCharacterEncoding();
        return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
    }

    @Override
    public String getContentType() {
        if (this.contentType == null) {
            this.contentType = super.getContentType();
        }
        return this.contentType;
    }

    @Override
    public String getParameter(String name) {
        if (this.content.size() == 0 && isFormPost()) {
            writeRequestParametersToCachedContent();
        }
        return super.getParameter(name);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        if (this.content.size() == 0 && isFormPost()) {
            writeRequestParametersToCachedContent();
        }
        return super.getParameterMap();
    }

    @Override
    public Enumeration<String> getParameterNames() {
        if (this.content.size() == 0 && isFormPost()) {
            writeRequestParametersToCachedContent();
        }
        return super.getParameterNames();
    }

    @Override
    public String[] getParameterValues(String name) {
        if (this.content.size() == 0 && isFormPost()) {
            writeRequestParametersToCachedContent();
        }
        return super.getParameterValues(name);
    }

    private boolean isFormPost() {
        return getContentType() != null
                && getContentType().contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                && HttpMethod.POST.equals(getHttpMethod());
    }

    private boolean isJsonPost() {
        return getContentType() != null
                && getContentType().contains(MediaType.APPLICATION_JSON_VALUE)
                && HttpMethod.POST.equals(getHttpMethod());
    }

    private void writeRequestParametersToCachedContent() {
        try {
            if (this.content.size() == 0) {
                String requestEncoding = getCharacterEncoding();
                Map<String, String[]> form = super.getParameterMap();
                for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext(); ) {
                    String name = nameIterator.next();
                    List<String> values = Arrays.asList(form.get(name));
                    for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext(); ) {
                        String value = valueIterator.next();
                        this.content.write(URLEncoder.encode(name, requestEncoding).getBytes());
                        if (value != null) {
                            this.content.write('=');
                            this.content.write(URLEncoder.encode(value, requestEncoding).getBytes());
                            if (valueIterator.hasNext()) {
                                this.content.write('&');
                            }
                        }
                    }
                    if (nameIterator.hasNext()) {
                        this.content.write('&');
                    }
                }
            }
        } catch (IOException ex) {
            throw new IllegalStateException("Failed to write request parameters to cached content", ex);
        }
    }

    private void writeRequestBodyToCachedContent() {
        try {
            StreamUtils.copy(super.getInputStream(), this.content);
        } catch (IOException ex) {
            throw new IllegalStateException("Failed to write request body to cached content", ex);
        }
    }

    public String getContentAsString() {
        if (this.content.size() == 0 && isJsonPost()) {
            writeRequestBodyToCachedContent();
        }
        return new String(this.content.toByteArray(), StandardCharsets.UTF_8);
    }

    @Override
    public ServletInputStream getInputStream() {
        if (this.content.size() == 0 && isJsonPost()) {
            writeRequestBodyToCachedContent();
        }
        return new CacheServletInputStream(this.content.toByteArray());
    }

    @Override
    public BufferedReader getReader() {
        if (this.content.size() == 0 && isJsonPost()) {
            writeRequestBodyToCachedContent();
        }
        return new BufferedReader(new InputStreamReader(this.content.getInputStream(), StandardCharsets.UTF_8));
    }

    private static class CacheServletInputStream extends ServletInputStream {

        private final ByteArrayInputStream byteArrayInputStream;

        public CacheServletInputStream(byte[] bytes) {
            this.byteArrayInputStream = new ByteArrayInputStream(bytes);
        }

        @Override
        public int read() {
            return this.byteArrayInputStream.read();
        }

        @Override
        public boolean isFinished() {
            return this.byteArrayInputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener listener) {
            throw new UnsupportedOperationException();
        }
    }
}